{ ---------------------------------------------------------------------

Title: Interscript Communication System

File: ISCS_V102.txt

Author: R.D.Villwock aka 'Big Bob'

First Written: January 29, 2007

Current Version: 1.01

Last Modified: April 26, 2007

----------------------------------------------------------------------}
{ This module can be used for both Interscript Communication and

  Intrascript pseudo Calls. The ISCS uses a messenger note (MIDI Note

  zero) and its associated event parameters (EPs) for communication

  and bi-directional data transfers. EP[3] of MsgNote functions as an

  operation descriptor, normally containing four fields as follows:

  

    MsgNote EVENT_PAR[3] Layout for Interscript Messages 

      Fields | MsgCode | Sender SID | ReplyCode | Client SID |

      Bits   | 31   27 | 26      16 | 15     11 | 10       0 |

      

  For interscript communication, the remaining event parameters, EP[0],

  EP[1], and EP[2] can be used to pass data forward or backward through

  the script chain. To use the ISCS, each hosting script is assigned a

  unique Script ID code, SID, which should not be used by any other

  hosting script. The allowable SID assignment range is from 1..2045.



  The functions 'SendMsg/D' are used to send a message to one specific

  Client (SID = 1..2045) and data can be carried both to the client

  and back to the sender. A handshake Reply from the client is used to

  inform the sender that the message was received and to provide info

  on how it was acted upon. Client message codes are defined and
  published by the Author of each client script.

  

  When the Client field is set to 2046 = No_SID (as it is by the
  'SendOmniMsg/D' functions), the ISCS sends an omni-message directed
  to all Clients. Omni messages carry data forward only and no Reply
  handshake is used. Omni-message codes are defined and published by
  the Sending script Author.

  

  The Intrascript 'Call' function sets the Client field to 2047 = PCALL

  In this situation, the descriptor format changes as shown below:

       

    MsgNote EVENT_PAR[3] Layout for Intrascript Process Calling 

      Fields |  User-Specified Selector Index   | Client SID = 2047 |

      Bits   | 31                            11 | 10              0 |
      
  NOTE: To use the ISCS with any given instrument, MsgNote must be an
        unused zone (in all groups). If you have an instrument that
        actually uses note zero you must edit the value of MsgNote to
        be one of the instrument's unused notes.
----------------------------------------------------------------------

                               Module Functions

----------------------------------------------------------------------

  io_init_ISCS(SID)   ISCS Data Constructor



    'SID' = Script ID number assigned to hosting script

  

  This function should be called at the end of the host's ISCS Interface
  Section (after defining the Script ID constants for all Sender/Client

  scripts to be associated with the host script). Host scripts with no

  registered SID assignment can utilize a limited subset of the ISCS
  services by using No_SID as their SID. Such scripts can then receive
  omni messages and use the pseudo-calling machinery of the ISCS but
  cannot send any messages (Client or Omni). }

function on_init_ISCS(SID)
                            { System Constants }
  declare const Echo := 31       { MsgCode to test for active presence of Client }
  declare const MsgNote := 0     { MIDI note # for Messenger }
  declare const No_SID := 2046   { Client code for omni-messages }   
  declare const PCall := 2047    { Client code for intracript calls }
                         { Data extraction masks }    
  declare const Parm_Mask := 0x1F { Bit mask for MsgCode/ReplyCode isolation }  
  declare const SID_Mask := 0x7FF { Bit mask for Sender/Client SID isolation }
  declare const Call_Mask := 0x1FFFFF     { Bit mask for Call Selector field }
                  { Standard Message Reply Code Assignments }
  declare const mrNONE  := 0    { Default set by Sender }
  declare const mrOK  := 1      { Normal successful handshake }
                          { ID Marking Constants }
  declare Mark[27] := (MARK_1,MARK_2,MARK_3,MARK_4,MARK_5,MARK_6,MARK_7, ...
                  MARK_8,MARK_9,MARK_10,MARK_11,MARK_12,MARK_13,MARK_14, ...
                  MARK_15,MARK_16,MARK_17,MARK_18,MARK_19,MARK_20)
  declare const ALL_HELD := MARK_28  { ID Mark reserved for pedal-held notes }
                            { Variables/Arrays }
  declare CallActv     { Call was triggered but hasn't 'returned' yet }
  declare CallID       { MsgNote ID for current pseudo-call, 0 = Idle }
  declare MsgActv      { Message was sent but no handshake reply yet }
  declare MsgID        { MsgNote ID for current interscript message, 0 = Idle }
  declare OmniMsgID    { MsgNote ID for current omni message }
  declare ReplyCode    { Specific-Client message reply code }
  declare ReplyData[3] { Client Data returned }
                { Self-SID of the hosting script }
  declare HostSID      { This is set to the host's uniquely  }
  HostSID := SID       {  assigned (ie Registered) Script ID }
end function { on_init_ISCS }

{ ISCS_NCB(MsgHandler,OmniHandler)

                    Insert ISCS_NCB at the head of the NCB if your

                    script needs to receive/respond to messages

                    (specific or omni) from earlier scripts.



     'MsgHandler' = Host script routine to process client-specific

                    messages from sender.

     'OmniHandler = Host script routine to process sender-specific

                    (omni) messages.

       

  'MsgHandler' decodes the 16-bit MsgCode/SenderSID fields, grabs any

  additional parameters needed (from EP[0]..EP[2]) and then takes the

  needed action and/or returns any requested data. Client-specific
  messages must always return an appropriate ReplyCode as a handshake
  to the Sender. For more information on writing 'MsgHandler', see
  comments near the end of this module. 

  

  'OmniHander' decodes the MsgCode/SenderSID fields, utilizing any

  needed parameters sent with the message (in EP[0]..EP[2]) and takes

  any needed action. No return handshake is required, nor can any
  data be returned. For more info on writing 'OmniHandler', see
  comments near the end of this module. }

                      

function ISCS_NCB(MsgHandler,OmniHandler)

  declare Client

  declare MsgCode

  declare Reply

  declare Sender

    

  if EVENT_NOTE = MsgNote    { ISCS MsgNote }

    Client := EVENT_PAR[3] .and. SID_Mask   { Extract Client field }
    if Client = PCall
      ignore_event(EVENT_ID) { Call messenger from prior script, }
      exit                   {  Inhibit further propagation      }
    end if
    if ((Client = No_SID) or (Client = HostSID)) { Omni or Specific msg }
      Sender := sh_right(EVENT_PAR[3],16) .and. SID_Mask   { Extract Sender }

      MsgCode := sh_right(EVENT_PAR[3],27) .and. Parm_Mask { Extract MsgCode }
    end if    
    if Client = No_SID     { Process as an Omni message }
      OmniHandler(Sender,MsgCode)
      exit
    end if

    if Client = HostSID      { Message is specifically for this host }

      ignore_event(EVENT_ID) { Stop further propagation }
      if MsgCode = Echo      { This is an 'are you there?' test }
        Reply := mrOK        { Simply Acknowledge active presence }
      else  { Use MsgHandler to decode message to this specific host }
        CallID := CallID     { NOP to avoid a null elso clause }

        MsgHandler(Sender,MsgCode,Reply)
      end if

      set_event_par(EVENT_ID,3,EVENT_PAR[3] .or. sh_left(Reply,11)) { Handshake }

    end if

  end if  { MsgNote }

end function { ISCS_NCB }

{ ISCS_RCB(CallHandler)   Insert ISCS_RCB at the head of the RCB if your

                          script sends messages using SendMsg/SendMsgD,

                          SendOmniMsg/SendOmniMsgD, or if your script

                          executes local processes using Call.



       'CallHandler' = Host script routine.



  'CallHandler' decodes the 21-bit 'Selector' field and executes the

  specified process. For more information on writing 'CallHandler',

  see comments near the end of this module. }

   

function ISCS_RCB(CallHandler)
  declare MsgCode

  declare n

  

  if EVENT_NOTE = MsgNote  { MsgNote release }
    ignore_event(EVENT_ID) { Inhibit further propagation }
    if EVENT_ID = MsgID    { Client message sent, extract any reply }

      ReplyCode := sh_right(EVENT_PAR[3],11) .and. Parm_Mask
      MsgID := 0           { Allow next SendMsg }
      if ((sh_right(EVENT_PAR[3],27) .and. Parm_Mask) = Echo)

        exit    { This is a TestEcho response, no data xfr involved }
      end if

      if ReplyCode # mrNone { SendMsg Reply Handshake code received }

        for n := 0 to 2     { Copy any ReplyData from Client to host array }

          ReplyData[n] := EVENT_PAR[n]

        end for

      end if
      exit
    end if
    if EVENT_ID = CallID  { This is an intrascript process 'call' }

      CallHandler(sh_right(EVENT_PAR[3],11) .and. Call_Mask) { Host-supplied }

      CallID := 0         { Allow next Call }
    end if
  end if  { MsgNote }

end function { ISCS_RCB }

{ Call(selector)    Execute the selected process in the RCB



        'selector' = An encoded value (21 bits) used to specify

                     which process will execute in the RCB.



  Use 'Call' to execute one of a number of common procedures in the RCB from

  various places throughout your code. The 'Selector' parameter is the 21-bit

  Process Selector field that you can format any way you like, such as to pass

  a case statement index and/or values to your 'CallHandler' routine. The

  Call function doesn't exit until the triggered process completes. See
  comments near the end of this module for more information on the 'Call'
  function and writing the 'CallHandler' routine. 

  

  CAUTION: 

     Due to KSP limitations, 'Call' cannot be used within a RCB }
  
function Call(selector)
  while CallActv # 0    { One Call customer at a time }
    wait(10)
  end while
  CallActv := 1  { Lockout new callers until 'return' }
  CallID := play_note(MsgNote,1,0,1)    { Trigger RCB }
  { Encode EP[3] with Selector/PCall fields }
  set_event_par(CallID,3,sh_left(selector,11) .or. PCall)
  while CallID # 0
    wait(5)      { Wait here for called process to finish }
  end while
  CallActv := 0  { Enable any waiting callers at next task switch }
end function { Call }

{ SendMsg(Client,MsgCode)     Send MsgCode to the specified Client Script



      'Client'  = SID (1..2045) for the receiving Script

      'MsgCode' = Encoded message (1..29) to be sent to the 'Client' Script



  Use SendMsg to send a message to or request data from another specific

  script higher in the chain. The permissible 'MsgCode' values should be

  published by the author of the specific Client Script. When SendMsg

  exits, the ReplyCode can be examined to see if the Client script has

  responded. If the Client is not active in the system or not functioning

  properly, the ReplyCode will be left set to the default value of mrNONE.

  If you request data from a Client script and it returns a ReplyCode of

  mrOK, the requested data will be in the local ReplyData array when

  SendMsg exits. If you want to send data along with the message, use

  the SendMsgD function. For more info on SendMsg and writing the

  'MsgHandler' routine, see comments near the end of this module.

  

  CAUTION: 

     Due to KSP limitations, 'SendMsg' cannot be used within a RCB }
  
function SendMsg(Client,MsgCode)
  while MsgActv # 0  { One message customer at a time }
    wait(10)
  end while
  MsgActv := 1     { Lockout other messages from host }
  MsgID := play_note(MsgNote,1,0,100) { Allow 100us for the chain to respond }
  { Encode EP[3] with MsgCode, Sender, Client and ReplyCode = mrNONE }
  set_event_par(MsgID,3,sh_left(MsgCode,27) + sh_left(HostSID,16) + Client)
  while MsgID # 0  { MsgID set to zero again in ISCS_RCB after 100usec } 
    wait(100)
  end while
  MsgActv := 0     { Enable next waiting message to be sent }
end function { SendMsg }

{ SendMsgD(Client,MsgCode,Data)   Send MsgCode along with Data to

                                  the specified Client Script

                              

       'Client' = SID (1..2045) for the targeted, receiver Script

      'MsgCode' = Encoded message (1..29) to be sent to the 'Client' Script

         'Data' = 3-word data array containing data you want to send

                  to the Client Script.

                  

  Use SendMsgD just as you would SendMsg but, in addition the Data parameter

  allows you to send the contents of a 3-word array to the Client script.

  If you don't need to send any data with your message, you should just use

  the SendMsg function. 



  CAUTION: 

     Due to KSP limitations, 'SendMsgD' cannot be used within a RCB }
    
function SendMsgD(Client,MsgCode,Data)

  declare n

  

  while MsgActv # 0  { One message customer at a time }

    wait(100)

  end while
  MsgActv := 1     { Lockout other messages from host }
  MsgID := play_note(MsgNote,1,0,100)  { Give Client 100us for a response }
  { Encode EP[3] with MsgCode, Sender, Client and ReplyCode = mrNONE }

  set_event_par(MsgID,3,sh_left(MsgCode,27) + sh_left(HostSID,16) + Client)
  for n := 0 to 2  { Copy sender's data to messenger note 's EP[0]..EP[2] }
    set_event_par(MsgID,n,Data[n])
  end for

  while MsgID # 0  { MsgID set to zero again in ISCS_RCB after 100usec }

    wait(100)

  end while

  MsgActv := 0     { Enable next waiting message to be sent }

end function { SendMsgD }

{ SendOmniMsg(MsgCode)     Send MsgCode to all higher Scripts



      'MsgCode' = Encoded message (1..29) to be sent to all Scripts



  Use SendOmniMsg to send a message to all scripts higher in the chain.

  The meaning of the 'MsgCode' values should be published by the author

  of the sending script). Since the message is for all scripts, no
  ReplyCode nor data is returned to the sender. If you want to send

  data along with the omni message, use the SendOmniMsgD function. For

  more info on SendMsg and writing the 'OmniHandler' routine, see

  comments near the end of this module.

  

  NOTE: 

     You may invoke SendOmniMsg from any callback including the RCB }



function SendOmniMsg(MsgCode)

  OmniMsgID := play_note(MsgNote,1,0,100) { Broadcast the message to all clients }

  { Encode EP[3] with MsgCode, Sender, Client and ReplyCode = mrNONE }

  set_event_par(OmniMsgID,3,sh_left(MsgCode,27) + sh_left(HostSID,16) + No_SID)
end function { SendOmniMsg }

{ SendOmniMsgD(MsgCode,Data)     Send MsgCode and Data to all higher Scripts



      'MsgCode' = Encoded message (1..29) to be sent to all Scripts

         'Data' = 3-word data array containing data you want to send

                  to the Client Script.



  Use SendOmniMsgD just as you would SendOmniMsg but, in addition the Data

  parameter allows you to send the contents of a 3-word array to the all

  higher scripts. If you don't need to send any data with your message,

  you should just use the SendOmniMsg function. 



  NOTE: 

     You may invoke SendOmniMsgD from any callback including the RCB }

  

function SendOmniMsgD(MsgCode,Data)
  declare n
  
  SendOmniMsg(MsgCode)  { Setup for omni broadcast }
  for n := 0 to 2  { Copy sender's data with messenger note's EP[0]..EP[2] }

    set_event_par(OmniMsgID,n,Data[n])

  end for  

end function { SendOmniMsgD }

{ TestEcho(Client)   Test for the 'active' presence of Client Script

                              

       'Client' = SID (1..2045) of the script being sought

                  

  Use this function to determine if any given Client script is in the
  active chain. If the script is present, the ISCS variable named ReplyCode
  will be set to mrOK = 1. If the script is not present (or is bypassed),
  ReplyCode will be set to mrNONE = 0. No ReplyData is returned.


  CAUTION: 

     Due to KSP limitations, 'TestEcho' cannot be used within a RCB }

function TestEcho(Client)
  SendMsg(Client,Echo)
end function  { TestEcho }
(*

---------------------------------------------------------------------------------

                        Information for Writing Handlers

---------------------------------------------------------------------------------

  If a hosting script uses ISCS_NCB you need to provide host-specific functions
  named MsgHandler and OmniHandler. While any name may be used for these routines
  (as long as you pass those same names when you call ISCS_NCB), for the sake of
  uniformity it is recommended that all Script Authors using the ISCS name these
  functions 'MsgHandler' and 'OmniHandler'. 
--------------------------------------------------------------------------------
                              Writing MsgHandler
--------------------------------------------------------------------------------
  The general format for MsgHandler is as follows:

  

  function MsgHandler(sender,msgcode,reply)

     :

     :

  end function

  

  Input parameters 'sender' and 'msgcode' are provided by the ISCS and contain

  the SID of the sender and the message code sent, respectively. MsgHandler is

  called from the NCB in the context of the messenger note's EVENT_ID. Thus,
  if the Sender has included any data with the message, you can obtain it from

  EVENT_PAR[0]..EVENT_PAR[2] as needed. You also need to return the ouput
  parameter 'reply'. There are a total of 32 possible reply codes, only two of
  which are pre-defined by by the ISCS as follows:

    

        mrNONE  = 0   This is the default reply (set by the sender)

          mrOK  = 1   This is the standard successful reply

        

  Reply codes 30 and 31 are reserved for future use by the ISCS. Therefore
  each Client-script Author is free to assign and publish the remaining codes
  from 2..29 to convey whatever is desired to the sender. Message code 31 is
  used by the ISCS TestEcho function and message codes 0 and 30 are reserved
  for future ISCS usage. Therefore, message codes from 1 to 29 are available

  to be specified and published by the Author of the receiving Client script.
  Usually, the body of the 'MsgHandler' function will consist of a select-case
  construct to 'decode' the permissible message codes and act upon them
  accordingly. However, the Sender ID is also available, should you want to
  respond only to certain senders and perhaps exclude others.

  

  If you have defined one or more message codes as data requests to your script,

  then you must return the requested data in EP[0]..EP[2]. Therefore, if there

  is sender-supplied data, you must utilize or copy it first before overwriting

  it with the data you return. The format of data received and returned is also

  specified and published by the Author of the message-receiving Client script.

  

  The Sending script, using the ISCS function SendMsg/D, expects to receive any

  requested data (along with an mrOK) within 100us. Should your script 'xyz'

  need more time to get the requested data ready, you can define and publish

  additional reply codes like 'xyzHOLD' or 'xyzDISCARD' along with some

  protocol (and possibly additional message codes) that the sender can follow

  to obtain the 'sluggish' data. Whether you can have data requests ready in

  time or not, it is essential that you promptly send a ReplyCode other than

  the default mrNONE. Otherwise, the Sender will receive the mrNONE code

  by default and the sender will then likely assume that your script is not

  present. In such a case, the sender script may switch to its standalone mode.

--------------------------------------------------------------------------------

                              Writing OmniHandler

--------------------------------------------------------------------------------   

  The general format for OmniHandler is as follows: 

    

  function OmniHandler(sender,msgcode)

     :

     :

  end function

  

  Input parameters 'sender' and 'msgcode' are provided by the ISCS and contain

  the SID of the sender and the message code sent respectively. OmniHandler is

  called from the NCB in the context of the messenger note's EVENT_ID. Thus,
  if the Sender has included any data with the message, you can obtain it from

  EP[0]..EP[2] as needed. Since omni messages are sent to all scripts, no

  ReplyCode is required nor can you return any data to the sender.

  

  Omni message codes 0, 30 and 31 are reserved by the ISCS for future use.
  Therefore, sending scripts can assign and publish any of the remaining
  message codes from 1 to 29.  Usually, the body of the 'OmniHandler'
  function will consist of a select-case construct to 'decode' the sender-
  defined message codes and act upon them accordingly. However, since
  omni-message codes are defined by the Authors of the sender scripts, you
  may need to 'nest' a series of decoding case statements for each sender
  you wish to receive omni-messages from. Your OmniHandler can thus decode
  up to 29 messages from each Sender you intend to work with.

  

  If your script only needs to receive one or the other but not both Client

  and Omni messages, you still need to supply both handlers. However, the
  body of the unused handler function can be left empty if desired.

  ---------------------------------------------------------------------------------

                               Writing CallHandler

--------------------------------------------------------------------------------- 

  If your script needs to use ISCS_RCB, you must provide a host-specific function

  named 'CallHandler'. The general format for this function is as follows:

  

  function CallHandler(selector)

     :

     :    

  end function

  

  If your script does not use the ISCS service for intrascript triggering of

  RCB-based processes, the 'CallHandler' function must still be supplied but

  the body of the function can be left empty. 

  

  The input parameter 'selector' is supplied by the ISCS and is the same value

  you pass with the 'Call' function. 'CallHandler' is called from the RCB in

  context with the EVENT_ID of the messenger note. Most often, the 'selector'
  parameter will simply be a named constant that 'CallHandler' can use with a
  select-case construct to execute some related procedure. However, Call
  messages are only used by the host script that sends them and thus need not
  be published. Therefore, you are free to use the 21-bit 'selector' value in
  whatever way you please. Since you will not likely need all 21 bits of the
  'selector' just to index a process, you could also 'pack' some parameters
  (or other information you might want to 'pass' to the triggered process) in
  the 'selector' if you wish.

  

  Also, since the Call function uses mutual-exclusion semaphore logic, you can

  be assured that any data arrays you setup for passing parameters back and
  forth will not be clobbered by another thread in your script that is also

  using the Call machinery. The most common expected use of the ISCS 'Call'

  feature will be to reduce overall compiled code size. The target processes

  need be inline-expanded only once in the RCB, yet they can be 'called' from

  multiple places throughout your script. 

*)
  

